package com.nulldev.util.web.YouTubeExtractor.extractors.next;

import static com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractorUtil.getHeatmap;
import static com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractorUtil.getYTInitialData;
import static com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractorUtil.isDVR;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTContinuation;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.isYTMusicVideo;

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.concurrency.threadIt.v4.asyncTasks.AsyncTaskCallback;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Date.Dates;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.fuckTracking.FuckTracking;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor.ABIFeatures;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor.ExtractorType;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.YoutubeExtractionOptions;
import com.nulldev.util.web.YouTubeExtractor.clarifications.Clarification;
import com.nulldev.util.web.YouTubeExtractor.comments.YTComments.YTCommentInitData;
import com.nulldev.util.web.YouTubeExtractor.endscreen.EndscreenEntry;
import com.nulldev.util.web.YouTubeExtractor.endscreen.EndscreenEntry.EntryType;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractionFunctions;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractorUtil;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.YTAlternativeMetadataType;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.YTIAPIClientType;
import com.nulldev.util.web.YouTubeExtractor.formats.AudioCodec;
import com.nulldev.util.web.YouTubeExtractor.formats.FileType;
import com.nulldev.util.web.YouTubeExtractor.formats.Format;
import com.nulldev.util.web.YouTubeExtractor.formats.VideoCodec;
import com.nulldev.util.web.YouTubeExtractor.items.YouTubeStoryboardParser;
import com.nulldev.util.web.YouTubeExtractor.items.YouTubeStoryboardParser.YouTubeStoryboard;
import com.nulldev.util.web.YouTubeExtractor.items.YouTubeVideoHeatmap;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata.DRMMetadata;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata.PlaybackMode;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata.ThumbnailSizeOption;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist.PlaylistEntry;
import com.nulldev.util.web.YouTubeExtractor.signatures.SignatureExtractor;
import com.nulldev.util.web.YouTubeExtractor.signatures.SignatureUpdates;
import com.nulldev.util.web.YouTubeExtractor.signatures.extractor.Extractor;
import com.nulldev.util.web.YouTubeExtractor.subtitles.YouTubeCaptions;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractedVideo;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractionResult;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo.FormatType;

public class YouTubeNextFEE extends IYouTubeExtractor {

	/**
	 * NFEEv1: REMOVED <br>
	 * NFEEv2: DONE, MERGED [active] <br>
	 * NFEE was updated to 1.0.15 in 2.2.
	 */

	private static final IExecutorManager td = ExecutorManager.get();
	public static final String VERSION = "1.0.18";

	private static String generateURL(final String videoID, final Map<String, Object> arguments) {
		final StringBuilder b = new StringBuilder("https://www.youtube.com/watch?v=");
		b.append(videoID);
		if (arguments.containsKey(MAP_TYPE_FORCE_ENGLISH_LANG_COMPAT))
			b.append("&hl=EN");
		if (arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS)) {
			b.append("&bpctr=");
			b.append(System.currentTimeMillis());
		}
		if (arguments.containsKey(MAP_TYPE_ENABLE_VERIFIED_SPOOFING)) {
			b.append("&has_verified=1");
		}
		return b.toString();
	}

	private static final Logger logger = LoggerFactory.getLogger();

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(videoID, null, options);
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, String extraArgs, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(IYouTubeExtractorConstants.convertFromLegacy(videoID, extraArgs, options));
	}

	@Override
	public Executable<ExtractedVideo> generateTask(final Map<String, Object> arguments) {
		if (!isValidMap(arguments))
			throw new IllegalArgumentException("Invalid map: " + arguments);
		final String videoID = (String) arguments.get(MAP_TYPE_VIDEO_ID);
		if (!isValidVideoID(videoID))
			throw new IllegalArgumentException("Invalid video ID: " + videoID);
		final ControllableFutureTask<ExtractedVideo> cnft = new ControllableFutureTask<ExtractedVideo>();
		final List<YouTubeVideo> results = FastLists.list(32);
		final variable<Integer> _index = new variable<Integer>(-1);
		logger.debug("=> Generating URL...");
		final StringBuilder extras = new StringBuilder();
		if (arguments.containsKey(MAP_TYPE_PLAYLIST_STR)) {
			extras.append("&list=");
			extras.append(arguments.get(MAP_TYPE_PLAYLIST_STR));
		}
		if (arguments.containsKey(MAP_TYPE_PLAYLIST_INDEX)) {
			extras.append("&index=");
			final int ix = (int) arguments.get(MAP_TYPE_PLAYLIST_INDEX);
			extras.append(ix);
			_index.set((int) arguments.get(MAP_TYPE_PLAYLIST_INDEX));
		}
		final String url = generateURL(videoID, arguments) + extras;
		logger.debug("URL: " + url);
		Entry[] _args = (!isEnabled(arguments, MAP_TYPE_DISABLE_CONSENT_COOKIE_COMPAT)) ? YouTubeExtractor.DEFAULT_COOKIES : null;
		if (isEnabled(arguments, MAP_TYPE_SEND_FAKE_COOKIES_COMPAT) || isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT)) {
			if (_args == null)
				_args = new Entry[0];
			_args = ArrayUtils.add(_args, new Entry("Cookie", StringsUtil.randomString(64)));
		}
		if (isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT) || isEnabled(arguments, MAP_TYPE_EMULATE_INSECURE_PROXY_COMPAT)) {
			if (_args == null)
				_args = new Entry[0];
			_args = ArrayUtils.addAll(_args, FuckTracking.protect());
		}
		final boolean use_v9_deciphering = !isEnabled(arguments, MAP_TYPE_DISABLE_V9_DECIPHERING);
		final boolean keep_ls_formats = isEnabled(arguments, MAP_TYPE_LIVESTREAMS_KEEP_RAW_STREAMS);
		final boolean disablePriorityPoint = isEnabled(arguments, MAP_TYPE_HEATMAP_DISABLE_PRIORITY_POINT);
		try {
			final boolean allowLateExtractions = isEnabled(arguments, MAP_TYPE_ENABLE_LATE_EXTRACTIONS);
			final boolean ECD_ENABLED = isEnabled(arguments, MAP_TYPE_ENABLE_EXPERIMENTAL_FEATURES);
			final boolean fallenBack = arguments.containsKey(MAP_TYPE_HAS_FALLEN_BACK);
			HttpClient.getString().url(url).timeout(Duration.ofSeconds(15)).headers(_args).createTaskAndRun()
					.addCallback(new AsyncTaskCallback<HttpResponse<String>>() {

						@Override
						public void onCompletion(final HttpResponse<String> _result) {
							if (_result.status() == 403) {
								IOUtils.closeQuietly(_result);
								cnft.complete(new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_VIDEO_NOT_FOUND")
										.setYTMessage("YouTube reports this video as non-existant."));
								return;
							}
							final String result = _result.dataThenClose();
							final String raw_json = (String) td.asyncSubmit(() -> {
								final String rn = StringsUtil.substringBetweenO(result, ">var ytInitialPlayerResponse = ", "};");
								if (rn == null) {
									final String f = StringsUtil.substringBetweenO(result, "ytplayer.config = ", ";ytplayer.load");
									if (f == null || f.isEmpty())
										return null;
									final String r = ("{\"" + f);
									return r.substring(2, r.length());
								}
								return rn + "}";
							}).waitTillCompletion();
							if (result.contains("verify_controversy")) {
								logger.warn("YouTubeNextFEE-rolling -> This is a 'controversial' video, retrying with bypass...'");
								arguments.put(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS, true);
								cnft.complete(generateTask(arguments).run_st());
								return;
							}
							if (raw_json == null || raw_json.equalsIgnoreCase("null")) {
								final boolean rjbad = (raw_json == null || raw_json.equalsIgnoreCase("null"));
								logger.debug("hasYtPlayerCfg: " + result.contains("ytplayer.config"));
								logger.debug("raw_json-bad: " + rjbad);
								if (rjbad)
									logger.debug(result);
								logger.error("YouTubeNextFEE-rolling -> Extraction failed! Trying age-gate API!");
								cnft.complete(YouTubeExtractor.get(ExtractorType.AGE_GATE).generateTask(arguments).run_st());
								return;
							}
							final UniJsonValue parsedJson = UniJsonValue.parse(StringsUtil.replace(raw_json, ";", "%3B"), true);
							if (arguments.containsKey(MAP_TYPE_ENABLE_DEBUG_API_RESPONSE_PRINT)) {
								if (parsedJson != null)
									parsedJson.println();
							} else if (arguments.containsKey(MAP_TYPE_ENABLE_DEBUG_FULL_RESPONSE_PRINT)) {
								JVM.println(result);
							}
							if (parsedJson == null || parsedJson.isNull()) {
								logger.debug("hasYtPlayerCfg: " + result.contains("ytplayer.config"));
								logger.debug("raw_json-bad: " + (raw_json == null || raw_json.equalsIgnoreCase("null")));
								cnft.fail(new NullPointerException("parsedJson is null!"));
								return;
							}
							final UniJsonValue args = parsedJson.contains("args") ? parsedJson.get("args") : parsedJson;
							UniJsonValue _metadataCore = (args.isNull() || !args.contains("player_response") ? args
									: UniJsonValue.parse(args.getString("player_response"), true));
							if (arguments.containsKey(MAP_TYPE_ENABLE_DEBUG_API_RESPONSE_PRINT)) {
								if (_metadataCore != null)
									_metadataCore.println();
							}
							YTAlternativeMetadataType useAlternativeMetadata = YTAlternativeMetadataType.NONE;
							long alternateMetadataStartTime = -1L;
							if (_metadataCore.contains("playabilityStatus") && !ArrayUtils.contains(NON_TRAILER_PLAYABILITY_STATUSES,
									_metadataCore.get("playabilityStatus").getString("status", "null"))) {
								if (_metadataCore.get("playabilityStatus").contains("errorScreen")
										&& _metadataCore.get("playabilityStatus").get("errorScreen").contains("playerLegacyDesktopYpcTrailerRenderer")) {
									useAlternativeMetadata = YTAlternativeMetadataType.LEGACY_TRAILER_RENDERER;
									_metadataCore = _metadataCore.mget(
											"playabilityStatus.errorScreen.playerLegacyDesktopYpcTrailerRenderer.ypcTrailer.ypcTrailerRenderer.unserializedPlayerResponse");
									logger.debug("[alternativeMetadata] Alternative metadata found, using that instead.");
								} else if (_metadataCore.get("playabilityStatus").contains("errorScreen")
										&& _metadataCore.get("playabilityStatus").get("errorScreen").contains("ypcTrailerRenderer")) {
									useAlternativeMetadata = YTAlternativeMetadataType.TRAILER_RENDERER_V2;
									if (_metadataCore.get("playabilityStatus").contains("liveStreamability")) {
										final UniJsonValue lsy = _metadataCore
												.mget("playabilityStatus.liveStreamability.liveStreamabilityRenderer.offlineSlate");
										if (lsy != null && lsy.contains("liveStreamOfflineSlateRenderer")) {
											alternateMetadataStartTime = lsy.get("liveStreamOfflineSlateRenderer").getLong("scheduledStartTime", -1);
										} else
											logger.warn("Unknown livestreamability: " + lsy);
									}
									_metadataCore = _metadataCore.mget("playabilityStatus.errorScreen.ypcTrailerRenderer.unserializedPlayerResponse");
									logger.debug("[alternativeMetadata] Alternative metadata found, using that instead.");
								} else if (_metadataCore.get("playabilityStatus").getString("status", "null").equalsIgnoreCase("CONTENT_CHECK_REQUIRED")
										&& !arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS)) {
									arguments.put(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS, true);
									cnft.complete(generateTask(arguments).run_st());
									return;
								} else if (_metadataCore.get("playabilityStatus").getString("status", "null").equalsIgnoreCase("LOGIN_REQUIRED")) {
									final boolean canFallback = arguments.containsKey(MAP_TYPE_FALLBACK_LAYER)
											? (arguments.get(MAP_TYPE_FALLBACK_LAYER) instanceof Integer
													&& ((int) arguments.get(MAP_TYPE_FALLBACK_LAYER) + 1) < 2)
											: true;
									if (!canFallback && arguments.containsKey(MAP_TYPE_HAS_FALLEN_BACK)) {
										cnft.complete(new ExtractedVideo(null, null, ExtractionResult.FAILED_AGE_RESTRICTED)
												.setMessage("FAILED_VIDEO_AGE_RESTRICTED").setYTMessage("YouTube reports this video as being age-gated."));
										return;
									} else {
										logger.error("YouTubeNextFEE-rolling -> This is an age-gated video! Trying age-gate API!");
										cnft.complete(YouTubeExtractor.get(ExtractorType.AGE_GATE).generateTask(arguments).run_st());
										return;
									}
								} else if (ArrayUtils.contains(NON_TRAILER_PLAYABILITY_STATUSES,
										_metadataCore.get("playabilityStatus").getString("status", "null"))) {
								} else {
									cnft.complete(new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
											.setYTMessage("YouTube reported the following playbility status: "
													+ _metadataCore.get("playabilityStatus").getString("status", "null")));
									logger.debug(_metadataCore.get("playabilityStatus"));
									return;
								}
							}
							logger.debug("-> NFEE::metadata-check");
							final Executable<Extractor> extractorTask = (Executable<Extractor>) td.asyncSubmit(() -> {
								try {
									if (!YouTubeExtractor.extractor.hasCachedItem()) {
										logger.debug("nfee::extractor: Using an assisted fetch...");
										return YouTubeExtractor.extractor.assistedGet(result);
									} else {
										final String playerURL = SignatureUpdates.getPlayerURL(result);
										logger.debug("nfee::playerURL: " + playerURL);
										final String variantsChecksum = YouTubeExtractorConstants.ENABLE_VARIANTS_CHECKSUM
												? SignatureUpdates.getVariantsChecksum(result)
												: null;
										logger.debug("nfee::variantsChecksum: " + variantsChecksum);
										if (SignatureUpdates.requiresUpdate(playerURL, variantsChecksum)) {
											if (YouTubeExtractor.extractor.hasCachedItem() && SignatureExtractor.ENABLE_EXTRACTOR_RECYCLE) {
												logger.warn("Data mismatch, Updating decipherer!");
												YouTubeExtractor.extractor.get().updateESD(SignatureExtractor.fromData(result, playerURL));
											} else {
												logger.warn("Data mismatch, Reaccquiring decipherer!");
												return YouTubeExtractor.extractor.reaccquire();
											}
										}
										return YouTubeExtractor.extractor.getAuto();
									}
								} catch (Exception e) {
									throw new RuntimeException(e);
								}
							});
							logger.debug("-> NFEE::extractorTask");
							final UniJsonValue metadataCore = _metadataCore;
							final UniJsonValue playabilityStatus = metadataCore.get("playabilityStatus");
							if (playabilityStatus != null && playabilityStatus.contains("status")
									&& !ArrayUtils.contains(ALLOWED_PLAYABILITY_STATUSES, playabilityStatus.getString("status", "OK"))) {
								final String status = playabilityStatus.getString("status", "OK");
								if ((int) getFlag(FLAG_EXTRACTOR_SUPPORTS_AGE_GATE) != FLAG_TRUE && status.equalsIgnoreCase("LOGIN_REQUIRED")) {
									cnft.complete(YouTubeExtractor.get(ExtractorType.AGE_GATE).generateTask(arguments).run_st());
									return;
								} else {
									final boolean canFallback = arguments.containsKey(MAP_TYPE_FALLBACK_LAYER)
											? (arguments.get(MAP_TYPE_FALLBACK_LAYER) instanceof Integer
													&& ((int) arguments.get(MAP_TYPE_FALLBACK_LAYER) + 1) < 2)
											: true;
									final int currentFBLayer = arguments.containsKey(MAP_TYPE_FALLBACK_LAYER)
											&& arguments.get(MAP_TYPE_FALLBACK_LAYER) instanceof Integer ? ((int) arguments.get(MAP_TYPE_FALLBACK_LAYER)) : 0;
									if (status.equalsIgnoreCase("LOGIN_REQUIRED") && !fallenBack && canFallback) {
										final Map<String, Object> newArgs = FastMaps.map(arguments);
										newArgs.put(MAP_TYPE_HAS_FALLEN_BACK, true);
										newArgs.put(MAP_TYPE_USE_CLIENT_TYPE,
												currentFBLayer == 0 ? YTIAPIClientType.TVHTML5_SIMPLE_EMBEDDED_PLAYER : YTIAPIClientType.EMBEDDED_ANDROID);
										if (!arguments.containsKey(MAP_TYPE_FALLBACK_LAYER))
											newArgs.put(MAP_TYPE_FALLBACK_LAYER, 0);
										else
											newArgs.put(MAP_TYPE_FALLBACK_LAYER, currentFBLayer + 1);
										cnft.complete(generateTask(newArgs).run_st());
										return;
									} else if (status.equalsIgnoreCase("LOGIN_REQUIRED") && !fallenBack && !canFallback) {
										cnft.complete(YouTubeExtractor.get("YTE.third_party.simple_age_restriction_bypass").generateTask(arguments).run_st());
										return;
									} else if (status.equalsIgnoreCase("CONTENT_CHECK_REQUIRED") && !fallenBack && canFallback) {
										final Map<String, Object> newArgs = FastMaps.map(arguments);
										newArgs.put(MAP_TYPE_HAS_FALLEN_BACK, true);
										newArgs.put(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS, true);
										if (!arguments.containsKey(MAP_TYPE_FALLBACK_LAYER))
											newArgs.put(MAP_TYPE_FALLBACK_LAYER, 0);
										else
											newArgs.put(MAP_TYPE_FALLBACK_LAYER, currentFBLayer + 1);
										// There seems to be no way to apply "bpctr" to YTIAPI, fallback to NFEE-rolling
										// for now.
										cnft.complete(YouTubeExtractor.get("YouTubeExtractor.NFEE_rolling_release").generateTask(newArgs).run_st());
										return;
									} else {
										cnft.complete(
												new ExtractedVideo(null, null, fallenBack ? ExtractionResult.FAILED_AGE_RESTRICTED : ExtractionResult.FAILED)
														.setMessage("FAILED_STAGE_GET_METADATA_CORE")
														.setYTMessage("YouTube returned playability status " + status + "."));
										return;
									}
								}
							}
							final UniJsonValue metadataObject = metadataCore.get("videoDetails");
							final Executable<YouTubeMetadata> metadata = td.asyncSubmit(() -> {
								final List<YouTubeCaptions> captions = FastLists.list();
								if (metadataCore.contains("captions")) {
									final UniJsonValue ct = metadataCore.getSafe("captions").getSafe("playerCaptionsTracklistRenderer")
											.getIgnoreNull("captionTracks");
									if (ct == null) {
										logger.error("Failed to load captions, captionTracks was null."); // probably no subtitles or contributions are only
																											// enabled.
									} else {
										final UniJsonIterableValue captionArray = ct.asArray();
										if (!captionArray.isNull()) {
											for (final UniJsonValue caption : captionArray) {
												final String captionURL = caption.getString("baseUrl");
												final String captionText = StringsUtil._capitalizeString(caption.get("name").getString("simpleText"));
												final String captionCode = caption.getString("languageCode");
												captions.add(new YouTubeCaptions(captionURL, captionText, captionCode));
											}
										}
									}
								}
								if (metadataObject == null)
									throw new RuntimeException("metadataObject == null!");
								UniJsonValue keywords = UniJsonValue.array();
								try {
									keywords = metadataObject.get("keywords").asArray();
								} catch (Exception _e) {
								}
								if (keywords == null) {
									logger.warn("Fixed keywords!");
									keywords = UniJsonValue.array();
								}
								final String _pr = args.toString();
								final boolean is360 = /*
														 * _pr.contains("360°") ||
														 */ SharedExtractorUtil.is360(_pr);
								final YouTubeMetadata _metadata = new YouTubeMetadata(videoID,
										StringsUtil.replace(metadataObject.getString("title", "null"), "%3B", ";"),
										StringsUtil.replace(metadataObject.getString("author", "null"), "%3B", ";"),
										metadataObject.getString("channelId", "null"), metadataObject.getLong("viewCount", -1),
										metadataObject.getLong("lengthSeconds", -1), metadataObject.getDouble("averageRating", 5), -1, -1, is360,
										keywords.toArray(), captions);
								if (metadataCore.contains("captions")) {
									final UniJsonValue rt = metadataCore.get("captions");
									try {
										_metadata.setRawCaptionsURL(rt.get("playerCaptionsRenderer").getString("baseUrl", null));
									} catch (Exception e) {
										logger.error("Failed to load base captions URL!");
									}
								}
								_metadata.setVR180(is360); // FIXME: Proper soluton.
								_metadata.limitedExtraction(result.contains("verify_controversy") || arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS));
								if (metadataCore.hasValue("microformat") && metadataCore.get("microformat").hasValue("playerMicroformatRenderer")) {
									final UniJsonValue mf = metadataCore.get("microformat").get("playerMicroformatRenderer");
									_metadata.getExtendedMetadata().uploadDate = mf.getString("uploadDate", null);
									_metadata.getExtendedMetadata().publishDate = mf.getString("publishDate", null);
									_metadata.getExtendedMetadata().isFamilyFriendly = mf.getBoolean("isFamilySafe");
									if (metadataCore.hasValue("playerConfig") && metadataCore.get("playerConfig").hasValue("audioConfig")) {
										final UniJsonValue pc = metadataCore.get("playerConfig").get("audioConfig");
										_metadata.getExtendedMetadata().loudnessDbRaw = pc.getFloat("loudnessDb", 0);
										_metadata.getExtendedMetadata().perceptualLoudnessDb = pc.getFloat("perceptualLoudnessDb", 0);
										if (_metadata.getExtendedMetadata().loudnessDbRaw == 0) {
											_metadata.getExtendedMetadata().loudnessDbRaw = pc.getFloat("perceptualLoudnessDb", 0);
										}
									}
								}
								_metadata.setMap(result);
								_metadata.setDescription(StringsUtil.replaceEach(metadataObject.getString("shortDescription", "A description wasn't found."),
										DESCRIPTION_REPLACEMENTS_INPUT, DESCRIPTION_REPLACEMENTS_OUTPUT));
								if (isYTMusicVideo(metadataObject.getString("shortDescription", "A description wasn't found."))) {
									_metadata.setPlaybackMode(PlaybackMode.YT_MUSIC);
								}
								if (metadataObject.contains("thumbnail")) {
									if (metadataObject.get("thumbnail").contains("thumbnails") && metadataObject.mget("thumbnail.thumbnails").isArray()) {
										int level = 0;
										for (final UniJsonValue v : metadataObject.mget("thumbnail.thumbnails").asArray()) {
											final String url = v.getString("url", null);
											if (url == null || url.isEmpty()) {
												logger.warn("Invalid thumbnail object: " + v);
												continue;
											}
											final int localLevel = ThumbnailSizeOption.getLevel(url);
											if (level < localLevel)
												level = localLevel;
										}
										_metadata.setThumbnailLevel(level);
									}
								}
								if (metadataCore.contains("storyboards")) {
									if (metadataCore.get("storyboards").contains("playerStoryboardSpecRenderer")) {
										String spec = metadataCore.mget("storyboards.playerStoryboardSpecRenderer").getString("spec", null);
										_metadata.setRawStoryboardURL(spec);
										if (spec != null) {
											try {
												final YouTubeStoryboard sb = YouTubeStoryboardParser.parse(spec);
												logger.trace("abi(5000): " + sb);
												_metadata.setStoryboardURL(sb.urlFor(sb.section(sb.getBestSectionSize())));
											} catch (Exception e) {
												logger.error("Failed to decipher storyboard!", e);
											}
										}
									} else if (metadataCore.get("storyboards").contains("playerLiveStoryboardSpecRenderer")) {
										String spec = metadataCore.mget("storyboards.playerLiveStoryboardSpecRenderer").getString("spec", null);
										_metadata.setRawStoryboardURL(spec);
										if (spec != null) {
											try {
												final YouTubeStoryboard sb = YouTubeStoryboardParser.parse(spec);
												logger.trace("abi(5000): " + sb);
												_metadata.setStoryboardURL(sb.urlFor(sb.section(sb.getBestSectionSize())));
											} catch (Exception e) {
												logger.error("Failed to decipher storyboard!", e);
											}
										}
									}
								}
								return _metadata;
							});
							logger.debug("-> NFEE::_metadata");
							final variable<Boolean> isVR180 = new variable<Boolean>(false);
							final LinkedTransferQueue<Executable<YouTubeVideo>> queue = new LinkedTransferQueue<Executable<YouTubeVideo>>();
							if (metadataCore.hasValue("streamingData")) {
								logger.debug("=> Awaiting decipherer...");
								boolean useCipher = true;
								try {
									useCipher = metadataCore.getSafe("videoDetails").bool("useCipher", true);
								} catch (Exception e) {
									// SILENT: useCipher is only used as an optimization.
								}
								if (useCipher) {
									logger.debug("extractor: " + extractorTask.waitFor(60, TimeUnit.SECONDS));
								}
								logger.debug("=> Processing formats...");
								final UniJsonValue streamingData = metadataCore.get("streamingData");
								if (streamingData.contains("formats")) {
									streamingData.get("formats").asArray().forEach((v) -> {
										SharedExtractionFunctions.generate_legacy_video(queue, results, v, extractorTask, isVR180, use_v9_deciphering,
												ECD_ENABLED);
									});
								}
								logger.debug("-> NFEE::legacy");
								streamingData.get("adaptiveFormats").asArray().forEach((v) -> {
									SharedExtractionFunctions.generate_adaptive_video(queue, results, v, extractorTask, isVR180, use_v9_deciphering,
											ECD_ENABLED);
								});
								logger.debug("-> NFEE::adaptive");
								if (!allowLateExtractions) {
									logger.debug("-> NFEE::await_queue");
									final List<Executable<?>> ox = td.awaitQueueFor(20, TimeUnit.SECONDS, queue);
									if (!ox.isEmpty()) {
										logger.error("Failed to fully wait on queue handling!");
										logger.error("Failed by: ");
										ArrayUtils.printLln(ox);
									}
								}
								if (streamingData.contains("dashManifestUrl")) {
									results.add(new YouTubeVideo(streamingData.getString("dashManifestUrl"), false, -1,
											new Format(-1, FileType.UNKNOWN, AudioCodec.NONE, 0, false)).setFormatType(FormatType.DASH_STREAM));
								}
								logger.debug("-> NFEE::dashCheck");
							}
							final Executable<UniJsonValue> ytInitialData = getYTInitialData(result);
							final variable<Playlist> pl = new variable<Playlist>(null);
							logger.debug("-> NFEE::pls");
							final Executable<?> pls = td.asyncSubmit(() -> {
								if (ytInitialData.waitTillCompletion() != null) {
									try {
										final UniJsonValue o = ytInitialData.result().get("contents").get("twoColumnWatchNextResults");
										if (o.hasValue("playlist")) {
											final UniJsonValue p = o.get("playlist").get("playlist");
											int currentIndex = p.getInt("currentIndex", -1);
											if (currentIndex == -1) {
												logger.error("Failed to track playlist index!");
											} else {
												logger.debug("current playlist index: " + currentIndex);
												pl.set(new Playlist(p.getString("title"), currentIndex));
												for (final UniJsonValue _v : p.get("contents").asArray()) {
													if (_v.hasValue("playlistPanelVideoRenderer")) {
														final UniJsonValue v = _v.get("playlistPanelVideoRenderer");
														final String _videoID = v.getString("videoId");
														final int index = v.get("navigationEndpoint").get("watchEndpoint").getInt("index");
														final String url = "/watch?v=" + _videoID + "&list="
																+ v.get("navigationEndpoint").get("watchEndpoint").getString("playlistId") + "&index="
																+ (index + 1);
														String title = "[FIXME] Unknown.";
														if (v.hasValue("title")) {
															if (v.get("title").hasValue("simpleText")) {
																title = v.get("title").getString("simpleText");
															} else {
																logger.warn("Missing simpleText in title object!");
																logger.warn(v.toString());
															}
														}
														String author = "[FIXME] Unknown.";
														if (v.hasValue("longBylineText")) {
															if (v.get("longBylineText").hasValue("runs")) {
																author = v.get("longBylineText").get("runs").get(0).getString("text");
															} else {
																logger.warn("Missing text in longBylineText.runs object!");
																logger.warn(v.toString());
															}
														}
														pl.object().addEntry(new PlaylistEntry(title, "https://i.ytimg.com/vi/" + _videoID + "/mqdefault.jpg",
																author, url, index));
													} else {
														continue;
													}
												}
												pl.object().makeCurrent(pl.object().at(currentIndex));
												try {
													pl.object().makeNext(pl.object().at(currentIndex + 1));
												} catch (Exception e) {
													if (e instanceof IndexOutOfBoundsException) {
														logger.warn("Failed to set next playlist value, assuming end of playlist.");
													} else {
														logger.error("Failed to get next playlist value!", e);
													}
												}
												logger.debug("Found playlist!");
											}
										}
									} catch (Exception e) {
										logger.error("[playlistGen] Failed to get ytInitialData!", e);
									}
								}
								return null;
							});
							final Executable<Object> supertags = td.asyncSubmit(() -> {
								final UniJsonValue uv = ytInitialData.waitFor(10, TimeUnit.SECONDS);
								if (uv == null) {
									return null;
								}
								try {
									metadata.waitTillCompletion().setChapters(SharedExtractorUtil.getChapters(uv));
								} catch (Exception e) {
									logger.error("[chapterGen] Failed to get ytInitialData!", e);
								}
								try {
									UniJsonValue supertag = uv.mget("contents.twoColumnWatchNextResults.results.results.contents");
									if (supertag != null && !supertag.isNull()) {
										try {
											supertag = supertag.get(0).get("videoPrimaryInfoRenderer").get("superTitleLink").get("runs");
										} catch (Exception __e) {
										}
									}
									if (supertag != null && !supertag.isNull() && supertag.isArray()) {
										final List<String> supertags_entries = FastLists.list();
										supertag.asArray().forEach((tag) -> {
											final String text = tag.getString("text", "");
											if (!text.isEmpty() && !text.equals(" "))
												supertags_entries.add(text);
										});
										metadata.waitTillCompletion().setSupertags(supertags_entries);
									}
								} catch (Exception e) {
									logger.error("[superTagGen] Failed to get ytInitialData!", e);
								}
								return null;
							});
							logger.debug("-> NFEE::endscreen");
							final Executable<List<EndscreenEntry>> endscreen = td.asyncSubmit(() -> {
								final List<EndscreenEntry> entries = FastLists.list();
								if (metadataCore.hasValue("endscreen")) {
									for (final UniJsonValue _v : metadataCore.get("endscreen").get("endscreenRenderer").get("elements").asArray()) {
										final UniJsonValue v = _v.get("endscreenElementRenderer");
										final String style = v.getString("style");
										if (style == null) {
											logger.error("Failed to get endscreen style.");
											continue;
										}
										if (style.equalsIgnoreCase("CHANNEL")) {
											continue; // Rejected implementation
										} else if (style.equalsIgnoreCase("PLAYLIST")) {
											try {
												final String fullURL = "/watch?v=" + v.get("endpoint").get("watchEndpoint").getString("videoId") + "&list="
														+ v.get("endpoint").get("watchEndpoint").getString("playlistId");
												entries.add(new EndscreenEntry(EntryType.PLAYLIST, fullURL,
														v.get("endpoint").get("watchEndpoint").getString("videoId"), v.get("title").getString("simpleText"),
														v.get("metadata").getString("simpleText")));
											} catch (Exception e) {
												logger.error("Failed to process PLAYLIST endscreen entry", e);
												logger.info(v);
											}
										} else if (style.equalsIgnoreCase("VIDEO")) {
											try {
												if (!v.get("endpoint").contains("watchEndpoint"))
													logger.warn("Unknown endpoint: " + v.get("endpoint"));
												final String fullURL = "/watch?v=" + v.get("endpoint").get("watchEndpoint").getString("videoId");
												entries.add(new EndscreenEntry(EntryType.VIDEO, fullURL,
														v.get("endpoint").get("watchEndpoint").getString("videoId"), v.get("title").getString("simpleText"),
														v.get("metadata").getString("simpleText")));
											} catch (Exception e) {
												logger.error("Failed to process VIDEO endscreen entry", e);
												logger.info(v);
											}
										} else if (style.equalsIgnoreCase("WEBSITE")) {
											try {
												final String fullURL = StringsUtil.replace(v.get("endpoint").get("urlEndpoint").getString("url"),
														"https://www.youtube.com", "");
												final String thumbnail = v.get("image").get("thumbnails").get(0).getString("url");
												entries.add(new EndscreenEntry(EntryType.WEBSITE, fullURL, "null", v.get("title").getString("simpleText"),
														v.get("metadata").getString("simpleText")).overrideImageURL(thumbnail));
											} catch (Exception e) {
												logger.error("Failed to process WEBSITE endscreen entry", e);
												logger.info(v);
											}
										} else {
											logger.warn("Ignored unknown endscreen style: " + style);
											logger.warn(v.toString());
										}
									}
								}
								return entries;
							});
							logger.debug("-> NFEE::drm");
							final Executable<?> drm = td.asyncSubmit(() -> {
								if (metadataCore.contains("streamingData") && metadataCore.get("streamingData").contains("licenseInfos")) {
									logger.debug("[DRM] Found DRM, attempting to generate metadata.");
									final UniJsonValue dr = metadataCore.get("streamingData");
									final UniJsonValue lis = dr.get("licenseInfos");
									final DRMMetadata.LicenseInfo[] lifs = new DRMMetadata.LicenseInfo[lis.length()];
									for (int i = 0; i < lis.length(); i++) {
										final UniJsonValue li = lis.get(i);
										lifs[i] = new DRMMetadata.LicenseInfo(li.getString("url"), li.getString("drmFamily"));
									}
									final DRMMetadata drmM = new DRMMetadata(dr.getString("drmParams"), dr.getString("dashManifestUrl"), lifs);
									metadata.waitTillCompletion().setDRMMetadata(drmM);
									if (metadataCore.contains("playerConfig") && metadataCore.get("playerConfig").contains("webDrmConfig")) {
										final UniJsonValue drmConfig = metadataCore.get("playerConfig").get("webDrmConfig");
										drmM.setUseInnertubeDrmService(drmConfig.getBoolean("useInnertubeDrmService"));
										drmM.setWidevineCertificate(drmConfig.getString("widevineServiceCert"));
									}
								}
								return null;
							});
							final variable<String> cxt = new variable<String>(null);
							logger.debug("-> NFEE::cmnts");
							final Executable<?> cmnts = td.asyncSubmit(() -> {
								if (ytInitialData.waitTillCompletion() != null) {
									try {
										final UniJsonValue preO = ytInitialData.result().mget("contents.twoColumnWatchNextResults");
										final UniJsonIterableValue o;

										if (preO.contains("secondaryResults") && !preO.contains("results")) {
											o = preO.mget("secondaryResults.secondaryResults.results").asArray();
										} else {
											o = preO.mget("results.results.contents").asArray();
										}

										final UniJsonValue clarification = o.first();
										if (clarification != null && !clarification.isNull() && isEnabled(arguments, MAP_TYPE_ENABLE_CLARIFICATIONS)) {
											if (clarification.get("itemSectionRenderer").get("contents").get(0).hasValue("clarificationRenderer")) {
												final UniJsonValue cr = clarification.get("itemSectionRenderer").get("contents").get(0)
														.get("clarificationRenderer");
												final String title = cr.get("contentTitle").get("runs").get(0).getString("text");
												final String text = cr.get("text").get("runs").get(0).getString("text");
												final String subText = cr.get("source").getString("simpleText");
												final String url = cr.get("endpoint").get("urlEndpoint").getString("url");
												metadata.waitTillCompletion().addClarification(new Clarification(title, text, subText, url));
											}
										}

										for (final UniJsonValue v : o) {
											if (v.hasValue("itemSectionRenderer")) {
												final UniJsonValue section = v.get("itemSectionRenderer");
												final String si = section.getString("sectionIdentifier");
												if (si.equalsIgnoreCase("comment-item-section")) {
													cxt.set(getYTContinuation(section));
													break;
												} else if (si.equalsIgnoreCase("comments-entry-point")) {
													/* Not available through this entrypoint. Use a different data source. */
													continue;
												} else {
													logger.warn("[cxt] Unknown itemSectionRenderer: " + section);
													continue;
												}
											} else {
												continue;
											}
										}

										if (cxt.isNull() && ytInitialData.result().contains("engagementPanels")) {
											/* Fallback to using engagement-panels */
											final UniJsonValue engagementPanels = ytInitialData.result().get("engagementPanels");

											UniJsonValue commentSection = null;
											for (final UniJsonValue candidate : engagementPanels.asArray()) {
												if (!candidate.contains("panelIdentifier")) {
													continue;
												}

												if (!candidate.getString("panelIdentifier").equalsIgnoreCase("engagement-panel-comments-section"))
													continue;

												commentSection = candidate;
												break;
											}

											if (commentSection != null) {
												final UniJsonValue content = commentSection
														/* */
														.get("content")
														/* */
														.get("sectionListRenderer")
														/* */
														.get("contents");

												final UniJsonValue isr = content.first()
														/* */
														.get("itemSectionRenderer");

												cxt.set(getYTContinuation(isr));

												logger.warn("Failed to use a standard way to accquire comments, used a fallback!");
											}
										}
									} catch (Exception e) {
										logger.error("[cmnts] Failed to get ytInitialData!", e);
									}
								}
								return null;
							});
							final Executable<String> cookiePayload = td.asyncSubmit(() -> {
								String _cookiePayload = "";
								for (final String cookie : _result.cookies()) {
									_cookiePayload += Splitters.splitString(cookie, ";")[0] + ";";
								}
								if (_cookiePayload.endsWith(";")) {
									_cookiePayload = _cookiePayload.substring(0, _cookiePayload.length() - 1);
								}
								return _cookiePayload;
							});
							logger.debug("-> NFEE::await[cmnts, drm, pls, supertags]");
							td.awaitFor(1, TimeUnit.SECONDS, cmnts, drm, pls, supertags);
							logger.debug("-> NFEE::alt_metadata");
							final Executable<YouTubeVideoHeatmap> heatmap = td.asyncSubmit(() -> {
								final UniJsonValue uv = ytInitialData.waitTillCompletion();
								return getHeatmap(uv, disablePriorityPoint);
							});
							metadata.waitTillCompletion().setHeatmap(heatmap);
							if (useAlternativeMetadata != YTAlternativeMetadataType.NONE) {
								metadata.waitTillCompletion().addClarification(new Clarification("Preview.", "The video you are viewing is a preview.",
										"nTube does not support paid movie playback.", null));
							}
							if (useAlternativeMetadata != YTAlternativeMetadataType.NONE && alternateMetadataStartTime > -1) {
								final String times = TimeUtil.formatSecondsL(alternateMetadataStartTime - System.currentTimeMillis() / 1000);
								metadata.waitTillCompletion()
										.addClarification(new Clarification("Upcoming content.", "This video/stream will be live in " + times + " from now.",
												"Stream Start: " + Dates.time((long) (alternateMetadataStartTime * 1000)).toString() + "", null));
								metadata.waitTillCompletion().setContentStartTime((long) (alternateMetadataStartTime * 1000));
							}
							if (metadataObject.hasValue("isUpcoming") && metadataObject.getBoolean("isUpcoming")) {
								logger.error("Failed -> This video isn't live yet!");
								String liveChatCont = null;
								try {
									liveChatCont = ytInitialData.waitTillCompletion().get("contents").get("twoColumnWatchNextResults").get("conversationBar")
											.get("liveChatRenderer").get("continuations").asArray().get(0).get("reloadContinuationData")
											.getString("continuation");
									logger.debug("liveChatCont: " + liveChatCont);
								} catch (Exception e) {
									logger.error("Failed to get liveChatCont!", e);
									logger.debug(ytInitialData.waitTillCompletion());
								}
								final long time = metadataCore.get("playabilityStatus").get("liveStreamability").get("liveStreamabilityRenderer")
										.get("offlineSlate").get("liveStreamOfflineSlateRenderer").getLong("scheduledStartTime", -1);
								final String times = TimeUtil.formatSecondsL(time - System.currentTimeMillis() / 1000);
								cnft.complete(new ExtractedVideo(results,
										metadata.waitTillCompletion().setContentStartTime((long) (time * 1000))
												.setEndscreenData(endscreen.waitTillCompletion()),
										ExtractionResult.FAILED).setMessage("ERROR_UPCOMING_STREAM")
										.setYTMessage("This video/stream will be live in " + times + " from now. (Stream Start: "
												+ Dates.time((long) (time * 1000)).toString() + ")")
										.setChatPayload(liveChatCont));
								return;
							} else if (metadataCore.hasValue("streamingData")) {
								final String liveChatCont = StringsUtil.substringBetweenO(result,
										"\"conversationBar\":{\"liveChatRenderer\":{\"continuations\":[{\"reloadContinuationData\":{\"continuation\":\"",
										"\",\"clickTrackingParams\":\"");
								final UniJsonValue streamingData = metadataCore.get("streamingData");
								final boolean isDVR = isDVR(metadataCore);
								final boolean isTrueLiveStream = metadataCore.get("videoDetails").bool("isLiveContent", false) && !isDVR;
								metadata.waitTillCompletion().setLiveStream(isTrueLiveStream);
								if (streamingData.hasValue("hlsManifestUrl")) {
									logger.debug("Parsing a live video...");
									final String hlsMetadata = streamingData.getString("hlsManifestUrl");
									final String dashManifestUrl = streamingData.getString("dashManifestUrl", null);
									if (isTrueLiveStream && !keep_ls_formats)
										results.clear();
									results.add(new YouTubeVideo(hlsMetadata, false, 0,
											new Format(0, FileType.M3U8, 30, 720, VideoCodec.H264, AudioCodec.AAC, -1, false)).setLivestream(true)
											.setInternalProxy(true).setFormatType(FormatType.M3U8_STREAM));
									if (dashManifestUrl != null)
										results.add(new YouTubeVideo(dashManifestUrl, false, 0,
												new Format(0, FileType.UNKNOWN, 30, 720, VideoCodec.H264, AudioCodec.AAC, -1, false)).setLivestream(true)
												.setInternalProxy(true).setFormatType(FormatType.DASH_STREAM));
									cnft.complete(new ExtractedVideo(results,
											metadata.waitTillCompletion().setLiveStream(true).setDVRStream(isDVR)
													.setEndscreenData(endscreen.waitTillCompletion()),
											ExtractionResult.SUCCESS).setChatPayload(liveChatCont).setClientType(YTIAPIClientType.WEB));
									return;
								}
							}
							logger.debug("-> NFEE::complete");
							cnft.complete(new ExtractedVideo(results, metadata.waitTillCompletion()
									/* */
									.setVR180(isVR180.get())
									/* */
									.setEndscreenData(endscreen.waitTillCompletion()), ExtractionResult.SUCCESS)
									/* */
									.setPlaylist(pl.object())
									/* */
									.setClientType(YTIAPIClientType.WEB)
									/* */
									.setLateExtraction(allowLateExtractions)
									/* */
									.setLateExtractionHold(allowLateExtractions ? td.asyncAwait(queue) : null)
									/* */
									.setCommentInitData(new YTCommentInitData(videoID, cookiePayload.waitTillCompletion(), "1",
											YouTubeExtractorConstants.CLIENT_VERSION, cxt.get(), SharedExtractorUtil.getXSRFToken(result))));
						}

						@Override
						public void onError(final Throwable e) {
							cnft.fail(e);
						}
					});
		} catch (Exception e) {
			cnft.fail(e);
		}
		return cnft;
	}

	@Override
	public Object getFlag(final int flag) {
		switch (flag) {
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAM_CHAT:
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAMS:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_SUPPORTS_AGE_GATE:
				return FLAG_FALSE;
			case FLAG_EXTRACTOR_SUPPORTS_ABI_2_2:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_GET_SUPPORTED_ABI:
				return 2.2f;
			default:
				return FLAG_NOT_SUPPORTED;
		}
	}

	@Override
	public String getID() {
		return "YTE.next.nextFEE";
	}

	@Override
	public int abiVersion() {
		/* Also supported: FAST_VIDEO_GENERATION, VIDEO_HEATMAP, VIDEO_START_TIME */
		return 5510;
	}

	@Override
	public ABIFeatures[] abiProvides() {
		// TODO: Implement our ABI_PROVIDES.
		return super.abiProvides();
	}

	@Override
	public void enableInstanceDebugging(final boolean value) {
		logger.debugEnabled.set(value);
	}
}
